/*
 * Project Porcupine
 * Copyright (C) 2012  Team Porcupine
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.towerdive.porcupine.controls;

import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.collision.PhysicsRayTestResult;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.GhostControl;
import com.jme3.collision.CollisionResults;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import com.towerdive.porcupine.Main;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Martin "quill18" Glaude <quill18@towerdive.com>
 */
public class CreatureControl extends AbstractControl {
    //Any local variables should be encapsulated by getters/setters so they
    //appear in the SDK properties window and can be edited.
    //Right-click a local variable to encapsulate it with getters and setters.

    
    /**
     * call visionGhostControl.getEncapsulatedObjects() to get a list of all
     * physics-enabled objects within vision range.
     */
    private GhostControl visionRadius = null;
    private Node rootNode;

    static final private float VISION_RANGE = 20f;
    
    public CreatureControl(Node rootNode) {
        this.rootNode = rootNode;
    }
    
    public List<CreatureControl> getVisibleCreatures() {

        List<CreatureControl> creatures = new ArrayList<CreatureControl>() {};
        
        if(visionRadius == null) {
            return creatures;
        }
        
        
        List<PhysicsCollisionObject> objOverlappingObjects = visionRadius.getOverlappingObjects();
        //Logger.getLogger("").log(Level.SEVERE, "Number of objects colliding with: " + objOverlappingObjects.size());
        
        for(int i=0; i < objOverlappingObjects.size(); i++) {
            PhysicsCollisionObject o = objOverlappingObjects.get(i);
            //Logger.getLogger("").log(Level.SEVERE, o.getClass().toString());
            Spatial s = (Spatial)o.getUserObject();
            //Logger.getLogger("").log(Level.SEVERE, "Found an object: {0}", s.getName());
            CreatureControl cc = (CreatureControl) s.getControl(CreatureControl.class);
            if(cc != null && cc != this) {
                // We found a creature!
                //Logger.getLogger("").log(Level.SEVERE, "{0} found a creature: {1}", new Object[]{spatial.getName(), s.getName()});
                if(hasLineOfSightPhysics(cc)) {
                    creatures.add(cc);
                }
            }
        }
        
        /**
         * TODO: Sort by distance!
         */

        return creatures;
    }
    
    /**
     * This uses a VISUAL line of sight check, which means it might "miss" if
     * the target's model is too high or low, or has a gap in the center.
     * Using this is not recommended, but is staying in for now, for reference
     * purposes.
     * @param target
     * @return 
     */
    private boolean hasLineOfSight(CreatureControl target) {
            // Reset results list.
            CollisionResults results = new CollisionResults();

            Vector3f origin = this.getSpatial().getWorldTranslation();
            Vector3f dir = target.getSpatial().getWorldTranslation().subtract(origin);
            
            /**
             * Move the origin to outside of ourselves
             */
            
            float myRadius = ((CapsuleCollisionShape)((CharacterControl)spatial.getControl(CharacterControl.class)).getCollisionShape()).getRadius();
            origin.addLocal( dir.normalize().mult(myRadius) );

            Ray ray = new Ray(origin, dir);
            
            // Collect intersections between ray and all nodes in results list.
            rootNode.collideWith(ray, results);
            //target.spatial.collideWith(ray, results);
                       
            // Use the results -- we rotate the selected geometry.
            if (results.size() > 0) {                
                Geometry geomHit = results.getClosestCollision().getGeometry();
                
                CreatureControl ccHit = geometryToCreatureControl(geomHit);
                
                if(ccHit != null) {
                    Logger.getLogger("").log(Level.SEVERE, "hasLineOfSight(): {0} sees: {1} (origin: {2}, dir: {3})", new Object[]{spatial.getName(), ccHit.spatial.getName(), origin, dir});
                }
                else {
                    Logger.getLogger("").log(Level.SEVERE, "hasLineOfSight(): {0} is blocked by: {1}", new Object[]{spatial.getName(), geomHit.getName()});
                }
            }
            else {
                Logger.getLogger("").log(Level.SEVERE, "hasLineOfSight(): {0} sees NOTHING (origin: {1}, dir: {2})", new Object[]{spatial.getName(), origin, dir});
            }
        
        return false;
    }    
    
    /**
     * This uses a PHYSICS line of sight check, using the target's collision
     * boxes.
     * TODO: Verify that we are colliding the correct collider, and not
     * a GhostCollider used for vision or auras or something.
     * @param target
     * @return 
     */
    private boolean hasLineOfSightPhysics(CreatureControl target) {
        CharacterControl myCharacter = (CharacterControl)spatial.getControl(CharacterControl.class);
        CharacterControl targetCharacter = (CharacterControl)target.spatial.getControl(CharacterControl.class);
        PhysicsSpace physicsSpace = myCharacter.getPhysicsSpace();
        
        /**
         * Note that if we are intersecting our target, rayTest() seems to return
         * an empty list.
         * TODO: Determine best response to this.  We could check for collision
         * if rayTest fails, or we could use collisions elsewhere to prevent
         * intersections altogether (though we should still check for null results
         * anyway).
         */
        List<PhysicsRayTestResult> results = physicsSpace.rayTest(myCharacter.getPhysicsLocation(), targetCharacter.getPhysicsLocation());
        Spatial closest = getNearestCollisionObject(results);
        if(closest != null && closest.getControl(CharacterControl.class) != null){
            if( closest.getControl(CreatureControl.class) == target) {
                //Logger.getLogger("").log(Level.SEVERE, "hasLineOfSight(): {0} sees: {1}", new Object[]{spatial.getName(), target.spatial.getName()});
                return true;
            }
        }
        
        return false;
    }

    /**
     * Iterates through a List<PhysicsRayTestResult> and returns the nearest object.
     * @param results The results of a physicsSpace.rayTest() call.
     * @return The closest spatial, or null if none.
     */
    private Spatial getNearestCollisionObject(List<PhysicsRayTestResult> results) {
        Spatial closest = null;
        float distance = 0;
        
        for(int cnt = 0; cnt < results.size(); cnt++){
            PhysicsCollisionObject o = results.get(cnt).getCollisionObject();
            Spatial s = (Spatial) o.getUserObject();
            float d = s.getLocalTranslation().distance(spatial.getLocalTranslation());
            if(closest == null || d < distance) {
                closest = s;
                distance = d;
            }
        }
        
        return closest;
    }
    
    static public CreatureControl geometryToCreatureControl(Spatial s) {
        CreatureControl cc = s.getControl(CreatureControl.class);
        
        while(cc == null && s.getParent() != null) {
            s = s.getParent();
            cc = s.getControl(CreatureControl.class);
        }
        
        return cc;
    }

    @Override
    protected void controlUpdate(float tpf) {
        if(visionRadius != null) {
            getVisibleCreatures();
        }
    }
    
    @Override
    public void setSpatial(Spatial spatial) {
        super.setSpatial(spatial);
        
        if(spatial != null) {
            /**
             * Creating ghost colliders will cause this warning on run:
             *  List<CreatureControl> creatures = new ArrayList<CreatureControl>() {};
             * Apparently this is "nothing to worry about":
             *  http://jmonkeyengine.org/groups/physics/forum/topic/warning-collisiondispatcher-needscollision-static-static-collision/
             */
            visionRadius = new GhostControl(new SphereCollisionShape(VISION_RANGE));
            
            /**
             * Move the ghost to a separate collision group, so that they don't detect
             * each other.
             */
            visionRadius.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_NONE);
            //visionRadius.removeCollideWithGroup(PhysicsCollisionObject.COLLISION_GROUP_01);
            //visionRadius.addCollideWithGroup(PhysicsCollisionObject.COLLISION_GROUP_02);

            spatial.addControl(visionRadius);

            BulletAppState bulletAppState = (BulletAppState) Main.app.getStateManager().getState(BulletAppState.class);
            bulletAppState.getPhysicsSpace().add(visionRadius);
        }

    }
    
    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        //Only needed for rendering-related operations,
        //not called when spatial is culled.
    }
    
    public Control cloneForSpatial(Spatial spatial) {
        CreatureControl control = new CreatureControl(rootNode);
        //TODO: copy parameters to new Control
        control.setSpatial(spatial);
        return control;
    }
    
    @Override
    public void read(JmeImporter im) throws IOException {
        super.read(im);
        InputCapsule in = im.getCapsule(this);
        //TODO: load properties of this Control, e.g.
        //this.value = in.readFloat("name", defaultValue);
    }
    
    @Override
    public void write(JmeExporter ex) throws IOException {
        super.write(ex);
        OutputCapsule out = ex.getCapsule(this);
        //TODO: save properties of this Control, e.g.
        //out.write(this.value, "name", defaultValue);
    }

}
